package com.cnbm.wcsbasic.protocol.netty.io;


import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
import com.cnbm.wcsbasic.protocol.netty.io.common.Package;
import com.cnbm.wcsbasic.protocol.netty.io.common.RcvPackage;
import com.cnbm.wcsbasic.protocol.netty.io.common.retry.FGRetryTemplate;
import com.cnbm.wcsbasic.protocol.netty.io.param.PlcVar;
import com.cnbm.wcsbasic.protocol.netty.io.syn.AsyncFuture;
import com.cnbm.wcsbasic.protocol.netty.io.syn.SendedList;
import com.cnbm.wcsbasic.protocol.netty.modbus.codec.ModbusTcpChannelInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.TimeUnit;

/**
 * @Desc: ""
 * @Author: caixiang
 * @DATE: 2022/1/15 13:01
 */
public enum FGClient {
    //如果要配置多个链接， local1 local2 .... 这样排下去好了

    local("上片机械手","192.168.0.51",502),
    //local("127.0.0.1",502,true),
    ;
    private String name;
    private String host;
    //默认 0   port
    private Integer port;
    private Bootstrap bootstrap;

    private ChannelFuture conn;
    private boolean isOnline;

    private static final Logger logger = LoggerFactory.getLogger("DCLOGGER");



    // ====  连接节点配置信息  =====   开始

    // ====  连接节点配置信息  =====   结束

    FGClient() {
    }

    //coreSize 是线程池的数量
    FGClient(String name, String host, Integer port){
        this.name = name;
        this.host = host;
        this.port = port;
        this.conn = null;
        initClient();
    }



    public String getHost(){
        return this.host;
    }
    public String getName(){
        return this.name;
    }
    public void setIsOnline(boolean isOnline){
        this.isOnline = isOnline;
    }
    /**
     * desc : 判断此链接 健康状况
     * return
     *         true  :  此tcp连接 正常
     *         false :  此tcp连接 异常
     * */
    public boolean isOnline(){
        return isOnline;
    }
    public void close(){
        //手动关闭连接，会出发InActivite 事件
        this.conn.channel().close();
    }

    /**
     *  （--）线程安全的（--）
     * PlcVar(byte[])  转  java对象    对照表
     * 单体变量
     * Bool       ===>      Boolean
     * LREAL      ===>      Double
     * REAL       ===>      Float
     * DATE       ===>      String(yyyy-MM-dd 这种形式的类型)
     * DTL        ===>      String("年-月-日-工作日-时-分-秒" 这种格式)
     * TIME       ===>      Integer(单位 ms)
     * USINT      ===>      Integer
     * SINT       ===>      Integer
     * UINT       ===>      Integer
     * INT        ===>      Integer
     * DINT       ===>      Integer
     * UINT       ===>      Long
     * Byte       ===>      Integer(有符号)(默认)
     *                     Integer(无符号)(后续扩展)
     * Char       ===>      Character
     * WChar      ===>      Character
     * String     ===>      String                      （特殊）


     * 数组变量
     * BoolArray      ===>      List<Boolean>
     * ByteArray      ===>      List<Byte>
     * WordArray      ===>      List<Integer>
     * DWordArray     ===>      List<Integer>
     * CharArray      ===>      List<Character>
     * SIntArray      ===>      List<Integer>
     * IntArray       ===>      List<Integer>
     * DIntArray      ===>      List<Integer>
     * UIntArray      ===>      List<Integer>
     * USIntArray     ===>      List<Integer>
     * UDIntArray     ===>      List<Long>
     * StringArray    ===>      String[]        （特殊）
     *
     * 如果返回null，就代表出现了异常，并且尝试了 retryMax 次数，并且尝试重置连接
     * */
    public RcvPackage read(PlcVar var) {
        return FGRetryTemplate.getInstance().execute(
                context -> {
                    try {
                        startConn();

                        //sendread 报文
                        //应该是ReadRequestFrame 继承 Packet 类，然后直接  Tio.bSend(this.conn, ReadRequestFrame)

                        Package mbPackage = var.toBytes();
                        AsyncFuture<Package> add = SendedList.add(mbPackage.getTransationId(),mbPackage);
                        this.conn.channel().writeAndFlush(mbPackage);


                        RcvPackage rsp = var.toObjectForRead(add.get(5000L, mbPackage.getTransationId()));
                        if(rsp.isOk()){
                            return rsp;
                        }else {
                            throw new RuntimeException(var.protocolName()+"  功能码错误，"+rsp.toString());
                        }

                    }catch (Throwable e) {
                        //e.printStackTrace();
                        String errMsg = "    [ FGclient - Read ]  [   "+name+"   host: "+ this.host +"   ]    (     occur err      ) errTime:  "+(context.getRetryCount()+1)+"  ;   read errMsg : "+e.getMessage()+"   ;  var :"+var.toString();
                        logger.info(errMsg);
                        throw new RuntimeException(errMsg);
                    }
                },
                context -> {
                    String errMsg = "    [ FGclient - Read ]  [   "+name+"   host: "+host+"   ]    (     finnal err     ) ;  ModbusTcp-Retry-Read : 已达到最大重试次数: "+ FGRetryTemplate.getMaxRetryTimes()+"   ; 现在尝试重新连接"+var.toString();
                    logger.info(errMsg);
                    //说明tcp连接异常了，close，出发inactive事件，从而出发重连
                    //close();
                    RcvPackage responseRes = new RcvPackage(false,errMsg,null);
                    return responseRes;
                }
        );
    }


    /**
     * （--）线程安全的（--）
     * eg :
     *      Object newValue = Boolean.FALSE
     *      s7Service.write(PlcVarActual.HeartBeat, newValue, S7Client.S7_1200);
     *
     * PlcVar(byte[])  转  java对象    对照表
     * 单体变量
     * Bool       ===>      Object newValue = Boolean.FALSE
     * LREAL      ===>      Object newValue = Boolean.FALSE
     * REAL       ===>      Object newValue = Boolean.FALSE
     * DATE       ===>      暂时没需求（有问题找我）
     * DTL        ===>      暂时没需求（有问题找我）
     * TIME       ===>      暂时没需求（有问题找我）
     * USINT      ===>      Object newValue = new Integer(1)
     * SINT       ===>      Object newValue = new Integer(1)
     * UINT       ===>      Object newValue = new Integer(1)
     * INT        ===>      Object newValue = new Integer(1)
     * DINT       ===>      Object newValue = new Integer(1)
     * UINT       ===>      Object newValue = new Integer(1)
     * Byte       ===>      Object newValue = 0x11
     *
     * Char       ===>      Object newValue = 'a'
     * WChar      ===>      Object newValue = '菜'
     * String     ===>      Object newValue = '你好啊'      （特殊）


     * 数组变量
     *      注意：在write的时候，你write的数量  一定要和  plc中存在的数量一一对应
     * BoolArray      ===>      boolean[] booleanArray = new boolean[2];  .... 赋予值
     * ByteArray      ===>      byte[] write_byteArrays = new byte[2];
     * WordArray      ===>      short[] shortArrays_content = new short[2];
     * DWordArray     ===>      int[] intArrays_content = new int[2];
     * CharArray      ===>      char[] charArrays_content = new char[2];
     * SIntArray      ===>      int[] sintArrays_content = new int[2];
     * IntArray       ===>      int[] iintArrays_content = new int[2];
     * DIntArray      ===>      int[] dintArrays_content = new int[2];
     * UIntArray      ===>      int[] uintArrays_content = new int[3];
     * USIntArray     ===>      int[] usintArrays_content = new int[3];
     * UDIntArray     ===>      int[] udintArrays_content = new int[3];
     * StringArray    ===>      String[] stringArrays_content = new String[3];
     * //如果有其他数据类型  这里没有找我扩展
     *
     *
     * */
    //todo 这里 ModbusTcpPlcVar 改成常量 模版模式
    public RcvPackage write(PlcVar var, Object newValue) {
        return FGRetryTemplate.getInstance().execute(
                context -> {
                    try {
                        startConn();
                        //sendread 报文
                        Package packages = var.toBytes(newValue);

                        AsyncFuture<Package> add = SendedList.add(packages.getTransationId(),packages);

                        this.conn.channel().writeAndFlush(packages);

                        //解析结果开始
                        RcvPackage rsp = var.toObjectForWrite(add.get(5000L, packages.getTransationId()));
                        if(rsp.isOk()){
                            return rsp;
                        }else {
                            throw new RuntimeException(var.protocolName()+"  功能码错误，"+rsp.toString());
                        }



                    }catch (Throwable e) {
                        //e.printStackTrace();
                        //String errMsg = name+", host: "+ this.host +"    ( occur err ) errTime:  "+(context.getRetryCount()+1)+"   ; var:"+var.toString()+"  ;   write errMsg : "+e.getMessage();
                        String errMsg = "    [ FGclient - Write ]  [   "+name+"   host: "+ this.host +"   ]    (     occur err      )   errTime:  "+(context.getRetryCount()+1)+"  ;   read errMsg : "+e.getMessage()+"   ;  var :"+var.toString();
                        logger.info(errMsg);
                        throw new RuntimeException(errMsg);
                    }
                },
                context -> {
                    String errMsg = "    [ FGclient - Write ]  [   "+name+"   host: "+host+"   ]    (     finnal err     )   ModbusTcp-Retry-Read : 已达到最大重试次数: "+ FGRetryTemplate.getMaxRetryTimes()+"   ; 现在尝试重新连接"+var.toString();
                    //说明tcp连接异常了，close，出发inactive事件，从而出发重连
                    //close();
                    logger.info(errMsg);
                    RcvPackage responseRes = new RcvPackage(false,errMsg,null);
                    return responseRes;
                }
        );
    }



    private synchronized void startConn() throws InterruptedException {
        if(this.conn == null){
            connect();
        }
    }

    public void connect() throws InterruptedException {
        //logger.info( name +"   "+ host +"  Try To Establish Communication Connection");
        this.conn = this.bootstrap.connect(this.host, this.port);
        this.conn.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {  // 没有连接成功，进行 “重连”
                    future.channel().eventLoop().schedule(() -> {
                        logger.info( "    [ FGclient                                              ]  [   "+name +"   "+ host +"   ]    (  connection    )   Establish Communication Failed, Try Again...");
                        try {
                            connect();
                        } catch (InterruptedException e) {
                            isOnline = false;
                            e.printStackTrace();
                            logger.info("     [ FGclient                                              ]  [   "+name +"   "+ host +"   ]    (  connection    )   Connect Exception Occurred , errMsg:  "+e.getMessage());
                        }
                    }, 3000, TimeUnit.MILLISECONDS);    // 设置 3s 进行一次 “重连”
                } else {
                    logger.info("    [ FGclient                                              ]  [   "+name +"   "+ host +"   ]    (  connection    )   Establish Communication Connection Successfully ...");
                    isOnline = true;
                }
            }
        });

        // Start the client.
        this.conn.sync();
        // 关闭对 当前通道 的监听 ( 除非其他地方调用了close方法 ) ( 在关闭之前保持TCP连接 )
        //this.conn.channel().closeFuture().sync();
    }



    /**
     * decs: 在项目启动的时候初始化 后面就不会初始化了
     * */
    private void initClient() {
        //NioEventLoopGroup 、 Bootstrap 这些资源其实是不用 release的，因为全局共用一份的你释放了 下次还得再new
        NioEventLoopGroup group = new NioEventLoopGroup();
        this.bootstrap = new Bootstrap();
        this.bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        //0 代表禁用 readerIdleTime 事件的监听
                        //这两个是固定的
                        pipeline.addLast(new IdleStateHandler(0,0,10, TimeUnit.SECONDS));
                        pipeline.addLast(new ReconnectAndHeartBeatHandler(FGClient.this));

                        //协议层的handler 这里要做以区分
                        pipeline.addLast(new ModbusTcpChannelInitializer());
                    }
                });
    }


}